/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.compose.ui.text.font

import androidx.compose.ui.util.fastFilter

/**
 * Given a [FontFamily], [FontWeight] and [FontStyle], matches the best font in the [FontFamily]
 * that satisfies the requirements of [FontWeight] and [FontStyle].
 *
 * For the case without font synthesis, applies the rules at
 * [CSS 4 Font Matching](https://www.w3.org/TR/css-fonts-4/#font-style-matching).
 */
internal class FontMatcher {

    /**
     * Given a [FontFamily], [FontWeight] and [FontStyle], matches the best font in the [FontFamily]
     * that satisfies the requirements of [FontWeight] and [FontStyle]. If there is not a font that
     * exactly satisfies the given constraints of [FontWeight] and [FontStyle], the best match will
     * be returned. The rules for the best match are defined in
     * [CSS 4 Font Matching](https://www.w3.org/TR/css-fonts-4/#font-style-matching).
     *
     * If no fonts match, an empty list is returned.
     *
     * @param fontList iterable of fonts to choose the [Font] from
     * @param fontWeight desired [FontWeight]
     * @param fontStyle desired [FontStyle]
     */
    fun matchFont(fontList: List<Font>, fontWeight: FontWeight, fontStyle: FontStyle): List<Font> {
        // check for exact match first
        fontList
            .fastFilter { it.weight == fontWeight && it.style == fontStyle }
            .let {
                // TODO(b/130797349): IR compiler bug was here
                if (it.isNotEmpty()) {
                    return it
                }
            }

        // if no exact match, filter with style
        val fontsToSearch = fontList.fastFilter { it.style == fontStyle }.ifEmpty { fontList }

        val result =
            when {
                fontWeight < FontWeight.W400 -> {
                    // If the desired weight is less than 400
                    // - weights less than or equal to the desired weight are checked in descending
                    // order
                    // - followed by weights above the desired weight in ascending order

                    fontsToSearch.filterByClosestWeight(fontWeight, preferBelow = true)
                }
                fontWeight > FontWeight.W500 -> {
                    // If the desired weight is greater than 500
                    // - weights greater than or equal to the desired weight are checked in
                    // ascending order
                    // - followed by weights below the desired weight in descending order
                    fontsToSearch.filterByClosestWeight(fontWeight, preferBelow = false)
                }
                else -> {
                    // If the desired weight is inclusively between 400 and 500
                    // - weights greater than or equal to the target weight are checked in ascending
                    // order
                    // until 500 is hit and checked,
                    // - followed by weights less than the target weight in descending order,
                    // - followed by weights greater than 500
                    fontsToSearch
                        .filterByClosestWeight(
                            fontWeight,
                            preferBelow = false,
                            minSearchRange = null,
                            maxSearchRange = FontWeight.W500,
                        )
                        .ifEmpty {
                            fontsToSearch.filterByClosestWeight(
                                fontWeight,
                                preferBelow = false,
                                minSearchRange = FontWeight.W500,
                                maxSearchRange = null,
                            )
                        }
                }
            }

        return result
    }

    @Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
    // @VisibleForTesting
    internal inline fun List<Font>.filterByClosestWeight(
        fontWeight: FontWeight,
        preferBelow: Boolean,
        minSearchRange: FontWeight? = null,
        maxSearchRange: FontWeight? = null,
    ): List<Font> {
        var bestWeightAbove: FontWeight? = null
        var bestWeightBelow: FontWeight? = null
        for (index in indices) {
            val font = get(index)
            val possibleWeight = font.weight
            if (minSearchRange != null && possibleWeight < minSearchRange) {
                continue
            }
            if (maxSearchRange != null && possibleWeight > maxSearchRange) {
                continue
            }
            if (possibleWeight < fontWeight) {
                if (bestWeightBelow == null || possibleWeight > bestWeightBelow) {
                    bestWeightBelow = possibleWeight
                }
            } else if (possibleWeight > fontWeight) {
                if (bestWeightAbove == null || possibleWeight < bestWeightAbove) {
                    bestWeightAbove = possibleWeight
                }
            } else {
                // exact weight match, we can exit now
                bestWeightAbove = possibleWeight
                bestWeightBelow = possibleWeight
                break
            }
        }
        val bestWeight =
            if (preferBelow) {
                bestWeightBelow ?: bestWeightAbove
            } else {
                bestWeightAbove ?: bestWeightBelow
            }
        return fastFilter { it.weight == bestWeight }
    }

    /** @see matchFont */
    fun matchFont(
        fontFamily: FontFamily,
        fontWeight: FontWeight,
        fontStyle: FontStyle,
    ): List<Font> {
        if (fontFamily !is FontListFontFamily)
            throw IllegalArgumentException(
                "Only FontFamily instances that presents a list of Fonts can be used"
            )

        return matchFont(fontFamily, fontWeight, fontStyle)
    }

    /**
     * Required to disambiguate matchFont(fontListFontFamilyInstance).
     *
     * @see matchFont
     */
    fun matchFont(
        fontFamily: FontListFontFamily,
        fontWeight: FontWeight,
        fontStyle: FontStyle,
    ): List<Font> {
        return matchFont(fontFamily.fonts, fontWeight, fontStyle)
    }
}
